// Copyright 2011 Google Inc. All Rights Reserved.

/**
 * @fileoverview This module provides instumentation to the YouTube player.
 * <br />
 * http://code.google.com/apis/youtube/flash_api_reference.html
 * <br />
 * http://code.google.com/p/swfobject/.
 * @author gavaletz@google.com (Eric Gavaletz)
 */


goog.provide('yt');


goog.require('datastore');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.net.EventType');
goog.require('goog.net.XhrIo');
goog.require('goog.ui.Button');
goog.require('goog.ui.Component');
goog.require('goog.ui.FlatButtonRenderer');
goog.require('goog.ui.media.FlashObject');
goog.require('page');
goog.require('results');
goog.require('track');


/**
 * The name for this test to be used when displaying results.
 *
 * @type {string}
 * @const
 */
yt.testName = 'video performance';


/**
 * An instance of goog.net.XhrIo for handling XMLHttpRequests.
 *
 * @type {goog.net.XhrIo}
 */
yt.xhr = new goog.net.XhrIo();


/**
 * The timestamp for when finish was called.
 *
 * @type {number}
 */
yt.finishTime;


/**
 * The timestamp for when the player was ready.
 *
 * @type {number}
 */
yt.startTime;


/**
 * The timestamp for when the video was cued.
 *
 * @type {number}
 */
yt.startDownloadTime;


/**
 * The process ID for the function polling the flash player.
 *
 * @type {number}
 */
yt.intervalID;


/**
 * The process ID for handling a player timeout.
 *
 * @type {number}
 */
yt.timeoutID;


/**
 * How long to wait before handling a player timeout.
 *
 * @type {number}
 */
yt.playerLoadTimeout = 30000; //ms


/**
 * The interval between successive polls to the flash player.
 *
 * Fastest internal update is 150 ms.
 *
 * @type {number}
 */
yt.sampleFreq = 1000; //ms


/**
 * The total video playback in seconds.
 *
 * @type {number}
 */
yt.totalTime = -1;


/**
 * The player state.
 *
 * @type {Array.<string>}
 */
yt.state = [];


/**
 * The player state time stamps.
 *
 * @type {Array.<number>}
 */
yt.stateTime = [];


/**
 * The quality for the iregular quality measurements.
 *
 * @type {Array.<string>}
 */
yt.quality = [];


/**
 * The timestamps for the iregular quality measurements.
 *
 * @type {Array.<number>}
 */
yt.qualityTime = [];


/**
 * The requested videoID.
 *
 * @type {string}
 */
yt.videoID;


/**
 * Sets the videoID.  Usually just used from the server.
 *
 * @param {string} s the videoID to be used.
 */
yt.setVideoID = function(s) {
  yt.videoID = s;
};
//used by the closure compiler to eport this function call
goog.exportProperty(window, 'ytSetVideoID', yt.setVideoID);


/**
 * The requested video quality (size).
 *
 * @type {string}
 */
yt.videoSize = 'default';


/**
 * The volume setting (0-100).
 *
 * @type {number}
 */
yt.volume = 100;


/**
 * The player height (px).
 *
 * @type {number}
 */
yt.height;


/**
 * The player width (px).
 *
 * @type {number}
 */
yt.width;


/**
 * Total time for YouTube experiment.
 *
 * @type {number}
 */
yt.endTime; //ms


/**
 * How offten did the video pause for buffering.
 *
 * @type {number}
 */
yt.bufferingCount;


/**
 * State variable for calculating bufferingCount.
 *
 * @type {boolean}
 */
yt.buffering;


/**
 * Amount of time between video request and begin of playback.
 *
 * @type {number}
 */
yt.joinTime; //ms


/**
 * Total time spent in the buffering state.
 *
 * @type {number}
 */
yt.timeBuffering; //Playback stopped for download (3) ms


/**
 * Total time spent in the unstarted state.
 *
 * @type {number}
 */
yt.timeUnstarted; //(-1) ms


/**
 * Total time spent in the ended state.
 *
 * @type {number}
 */
yt.timeEnded; //(0) ms


/**
 * Total time spent in the paused state.
 *
 * @type {number}
 */
yt.timePaused; //(2) ms


/**
 * Total time spent in the playing state.
 *
 * @type {number}
 */
yt.timePlaying; //(1) ms


/**
 * Total time spent in the cued state.
 *
 * @type {number}
 */
yt.timeCued; //(5) ms


/**
 * The ratio of time spent buffering to total time.
 *
 * @type {number}
 */
yt.bufferingRatio;  //ratio


/**
 * The frequency of buffering events.
 *
 * @type {number}
 */
yt.bufferingRate; //buffering events / second


/**
 * Updates the status of the page when an error is thrown by the player.
 *
 * @param {number} errorCode The error code.
 */
yt.onError = function(errorCode) {
  datastore.logError('ytError', errorCode);
  page.progressSetTestMessage(nsi18n('ytPlayerError') + errorCode);

  try {
    yt.finish();
  }
  catch (err) {
    datastore.logError('ytFinishError', err);
    clearInterval(yt.intervalID);
    page.progressClear();
    if (yt.nextFunction) {
      yt.nextFunction();
    }
  }
};
//used by the closure compiler to eport this function call
goog.exportProperty(window, 'ytonError', yt.onError);


/**
 * Handels the fatal error when the player fails to load.
 */
yt.onLoadError = function() {
  datastore.logError('ytError', 'player failed to load');
  page.progressSetTestMessage(nsi18n('ytPlayerError') + nsi18n('ytErrLoad'));

  page.progressClear();
  if (yt.nextFunction) {
    yt.nextFunction();
  }
};


/**
 * Updates the status of the page when an error is thrown by the player.
 *
 * @param {number} event The error code.
 */
yt.onIframeError = function(event) {
  datastore.logError('ytError', event.data);
  page.progressSetTestMessage('Player ERROR ' + event.data);

  try {
    yt.finish();
  }
  catch (err) {
    datastore.logError('ytFinishError', err);
    clearInterval(yt.intervalID);
    page.progressClear();
    if (yt.nextFunction) {
      yt.nextFunction();
    }
  }
};
//used by the closure compiler to eport this function call
goog.exportProperty(window, 'ytonIframeError', yt.onIframeError);


/**
 * Store the state information when it changes to reduce interval overhead.
 *
 * Also refresh the totalBytes and totalTime because this can change, but not
 * very often.
 *
 * @param {number} newState The state code.
 */
yt.onStateChange = function(newState) {
  yt.stateTime.push(new Date().getTime());
  yt.state.push(newState);
  if (newState == 1) {
    yt.totalTime = yt.player.getDuration();
  }
  if (newState == 0) {
    yt.totalTime = yt.player.getDuration();
    yt.finishExp();
  }
};
//used by the closure compiler to eport this function call
goog.exportProperty(window, 'ytonStateChange', yt.onStateChange);


/**
 * Store the state information when it changes to reduce interval overhead.
 *
 * Also refresh the totalBytes and totalTime because this can change, but not
 * very often.
 *
 * @param {number} event The state code.
 */
yt.onIframeStateChange = function(event) {
  yt.stateTime.push(new Date().getTime());
  yt.state.push(event.data);
  if (event.data == 1) {
    yt.totalTime = yt.player.getDuration();
  }
  if (event.data == 0) {
    yt.totalTime = yt.player.getDuration();
    yt.finishExp();
  }
};
//used by the closure compiler to eport this function call
goog.exportProperty(window, 'ytonIframeStateChange', yt.onIframeStateChange);


/**
 * Stores the quality information when it changes to reduce interval overhead.
 *
 * If the player ever starts to use adaptive streaming this will be more
 * important.
 *
 * @param {string} newQuality The new playback quality.
 */
yt.onQualityChange = function(newQuality) {
  yt.qualityTime.push(new Date().getTime());
  yt.quality.push(newQuality);
};
//used by the closure compiler to eport this function call
goog.exportProperty(window, 'ytonQualityChange', yt.onQualityChange);


/**
 * Stores the quality information when it changes to reduce interval overhead.
 *
 * If the player ever starts to use adaptive streaming this will be more
 * important.
 *
 * @param {string} event The new playback quality.
 */
yt.onIframeQualityChange = function(event) {
  yt.qualityTime.push(new Date().getTime());
  yt.quality.push(event.data);
};
//used by the closure compiler to eport this function call
goog.exportProperty(window, 'ytonIframeQualityChange',
    yt.onIframeQualityChange);


/**
 * Records information about the current state of the player.
 */
yt.updatePlayerInfo = function() {
  page.progressSetProgressBar(yt.player.getCurrentTime() / yt.totalTime * 100);
};


/**
 * Sets up the experiment once the player is loaded.
 */
yt.startExp = function() {
  page.progressSetTestMessage(nsi18n('ytTestMessageMeas'));
  page.progressSetProgressBar(0);

  yt.startTime = new Date().getTime();
  yt.intervalID = setInterval(yt.updatePlayerInfo, yt.sampleFreq);
  yt.qualityTime.push(yt.startTime);
  yt.quality.push(yt.player.getPlaybackQuality());
  yt.player.cueVideoById(yt.videoID);
  yt.player.setPlaybackQuality(yt.videoSize);
  yt.player.playVideo();
  yt.player.mute();
  yt.isMuted = true;
  yt.startDownloadTime = new Date().getTime();
};


/**
 * Stores all the raw data and sends it to the server.
 */
yt.finishExp = function() {
  yt.finishTime = new Date().getTime();
  clearInterval(yt.intervalID);

  datastore.json['ytPlayerVersion'] = yt.playerVersion;
  datastore.json['ytStartTime'] = yt.startTime;
  datastore.json['ytFinishTime'] = yt.finishTime;
  datastore.json['ytStartDownloadTime'] = yt.startDownloadTime;
  datastore.json['ytSampleFreq'] = yt.sampleFreq;
  datastore.json['ytTotalTime'] = yt.totalTime;
  datastore.json['ytVideoID'] = yt.videoID;
  datastore.json['ytVideoSize'] = yt.videoSize;
  datastore.json['ytHeight'] = yt.height;
  datastore.json['ytWidth'] = yt.width;
  datastore.json['ytStateTime'] = yt.stateTime;
  datastore.json['ytState'] = yt.state;
  datastore.json['ytQualityTime'] = yt.qualityTime;
  datastore.json['ytQuality'] = yt.quality;

  page.progressClear();
  if (yt.nextFunction) {
    yt.nextFunction();
  }
};


/**
 * Calculates the join time for the video.
 *
 * @param {Array.<string>} state the state change.
 * @param {Array.<number>} stateTime the time stamps for a state change.
 * @param {number} startDownloadTime the time the video started downloading.
 *
 * @return {number} the join time for the video.
 */
yt.calcJoinTime = function(state, stateTime, startDownloadTime) {
  for (i = 0; i < stateTime.length; i++) {
    if (state[i] == '1') {
      return stateTime[i] - startDownloadTime;
    }
  }
};


/**
 * Calculates the buffering count.
 *
 * @param {Array.<string>} state the state change.
 *
 * @return {number} the buffering count.
 */
yt.calcBufferingCount = function(state) {
  var bufferingCount = 0;
  var buffering = false;
  for (i = 0; i < state.length; i++) {
    if (buffering && state[i] != '3') {
      buffering = false;
    }
    else if (!(buffering) && state[i] == '3') {
      buffering = true;
      bufferingCount += 1;
    }
  }
  return bufferingCount;
};


/**
 * Calculates the statistics for the different playback states.
 *
 * @param {Array.<string>} state the state change.
 * @param {Array.<number>} stateTime the time stamps for a state change.
 *
 * @return {Object.<string, number>} the time spent in each state.
 */
yt.calcStateStats = function(state, stateTime) {
  var stats = {};
  stats.timeBuffering = 0; //Playback stopped for download (3) ms
  stats.timeUnstarted = 0; //(-1) ms
  stats.timeEnded = 0; //(0) ms
  stats.timePaused = 0; //(2) ms
  stats.timePlaying = 0; //(1) ms
  stats.timeCued = 0; //(5) ms
  stateTime.push(stateTime[stateTime.length - 1]);
  for (i = 0; i < stateTime.length - 1; i++) {
    if (state[i] == -1) {
      stats.timeUnstarted += (stateTime[i + 1] - stateTime[i]);
    }
    else if (state[i] == 5) {
      stats.timeCued += (stateTime[i + 1] - stateTime[i]);
    }
    else if (state[i] == 3) {
      stats.timeBuffering += (stateTime[i + 1] - stateTime[i]);
    }
    else if (state[i] == 1) {
      stats.timePlaying += (stateTime[i + 1] - stateTime[i]);
    }
    else if (state[i] == 0) {
      stats.timeEnded += (stateTime[i + 1] - stateTime[i]);
    }
    else if (state[i] == 2) {
      stats.timePaused += (stateTime[i + 1] - stateTime[i]);
    }
  }
  stateTime.pop();
  return stats;
};


/**
 * Calculates the buffering ratio.
 *
 * @param {Object.<string, number>} stats the time spent in each state.
 * @param {number} startTime the starting time stamp.
 * @param {number} finishTime the finishing time stamp.
 *
 * @return {number} the buffering ratio.
 */
yt.calcBufferingRatio = function(stats, startTime, finishTime) {
  return bufferingRatio = (stats.timeBuffering + stats.timeCued) /
      (finishTime - startTime);  //ratio
};


/**
 * Calculates the buffering rate.
 *
 * @param {number} bufferingCount the number of buffering events.
 * @param {number} startTime the starting time stamp.
 * @param {number} finishTime the finishing time stamp.
 *
 * @return {number} the buffering rate.
 */
yt.calcBufferingRate = function(bufferingCount, startTime, finishTime) {
  return bufferingCount / ((finishTime - startTime) / 1000.0); //events/second
};


/**
 * Displays results based on the data stored in the datastore.
 */
yt.results = function() {
  if (datastore.json['errors']['ytError'] == 'MOBILE unsupported') {
    var errDescripDiv = goog.dom.createDom('div', {'class': 'message'},
        nsi18n('ytErrDescrip'));
    var contentDiv = goog.dom.createDom('div', {}, errDescripDiv);
    page.resultsAppend(yt.testName, contentDiv);
  }
  else if (typeof datastore.json['ytState'] === 'undefined') {
    return;
  }

  var bufCount = yt.calcBufferingCount(datastore.json['ytState']);

  var stats = yt.calcStateStats(datastore.json['ytState'],
                                datastore.json['ytStateTime']);

  var stateDisplayDiv = goog.dom.createDom('div', {'class': 'displayChart'});
  var stateDescripDiv = goog.dom.createDom('div', {'class': 'message'},
      nsi18n('ytStateDescrip'));
  var stateTableDisplayDiv = goog.dom.createDom('div', {'class': 'table'});
  var otherDescripDiv = goog.dom.createDom('div', {'class': 'message'},
      nsi18n('ytOtherDescrip'));
  var otherTableDisplayDiv = goog.dom.createDom('div', {'class': 'table'});
  var contentDiv = goog.dom.createDom('div', {}, stateDescripDiv,
      stateDisplayDiv, stateTableDisplayDiv, otherDescripDiv,
      otherTableDisplayDiv);
  page.resultsAppend(yt.testName, contentDiv);

  var stateLables = [nsi18n('Unstarted'), nsi18n('Cued'), nsi18n('Buffering'),
        nsi18n('Playing'), nsi18n('Paused'), nsi18n('Ended')];
  var stateValues = [stats.timeUnstarted, stats.timeCued, stats.timeBuffering,
    stats.timePlaying, stats.timePaused, stats.timeEnded];
  var stateDataObj = results.dataToObj(stateLables, stateValues,
      nsi18n('Player State'), nsi18n('Time (ms)'), 'string', 'number');
  results.pieChart(stateDataObj, nsi18n('ytChartTitle'), stateDisplayDiv);
  results.table(stateDataObj, stateTableDisplayDiv);
  var otherLables = [nsi18n('Player Version'), nsi18n('Video ID'),
        nsi18n('Total Time (sec)'), nsi18n('Video Size (nominal)'),
        nsi18n('Join Time (sec)'), nsi18n('Buffering Ratio'),
        nsi18n('Buffering Rate (events/sec)'),
        nsi18n('Buffering Count (events)')];
  var otherValues = [datastore.json['ytPlayerVersion'],
    datastore.json['ytVideoID'],
    datastore.json['ytTotalTime'],
    datastore.json['ytVideoSize'],
    (yt.calcJoinTime(datastore.json['ytState'],
        datastore.json['ytStateTime'],
        datastore.json['ytStartDownloadTime']) / 1000.0),
    yt.calcBufferingRatio(stats, datastore.json['ytStartTime'],
        datastore.json['ytFinishTime']),
    yt.calcBufferingRate(bufCount, datastore.json['ytStartTime'],
        datastore.json['ytFinishTime']),
    bufCount];
  var otherDataObj = results.dataToObj(otherLables, otherValues,
      nsi18n('Field'), nsi18n('Value'), 'string', 'number');
  results.table(otherDataObj, otherTableDisplayDiv);
};


/**
 * Changes settings on the YouTube player once it is ready.
 *
 * This function seems to need to be in the global name space and that is why
 * it is not part of the yt namespace.
 *
 * @param {string} playerId The HTML id for the tag to be replaced.
 */
onYouTubePlayerReady = function(playerId) {
  clearTimeout(yt.timeoutID);
  yt.player = yt.flash.getFlashElement();
  yt.setupProgress += 50;
  page.progressSetProgressBar(yt.setupProgress);
  yt.player.addEventListener('onStateChange', 'ytonStateChange');
  yt.player.addEventListener('onError', 'ytonError');
  yt.player.addEventListener('onPlaybackQualityChange', 'ytonQualityChange');
  if (yt.videoID && !yt.startTime) {
    yt.startExp();
  }
  yt.playerVersion = 'flash';
};
goog.exportProperty(window, 'onYouTubePlayerReady', onYouTubePlayerReady);


/**
 * Starts the experiment once the player is loaded and the videoID is set.
 *
 * @this {goog.net.XhrIo}
 * @param {goog.events.Event} e The event that triggered this call.
 */
yt.videoIDReady = function(e) {
  yt.setupProgress += 25;
  page.progressSetProgressBar(yt.setupProgress);
  yt.videoID = this.getResponseText();
  if (yt.playerVersion && !yt.startTime) {
    yt.startExp();
  }
};
// Register yt.videoIDReady with yt.xhr.
goog.events.listen(yt.xhr, goog.net.EventType.COMPLETE, yt.videoIDReady);


/**
 * Loads the player and sets everything into motion.
 *
 * http://closure-library.googlecode.com/svn/docs/class_goog_ui_media_FlashObject.html
 */
yt.start = function() {

  if (track.isMobile()) {
    datastore.logError('ytError', 'MOBILE unsupported');
    if (yt.nextFunction) {
      yt.nextFunction();
    }
    else {
      return;
    }
  }

  //Be ready for the player to timeout and not load.
  yt.timeoutID = setTimeout(yt.onLoadError, yt.playerLoadTimeout);

  page.progressSetTestName(yt.testName);
  page.progressSetTestMessage(nsi18n('ytTestMessageLoad'));
  yt.setupProgress = 0;
  page.progressSetProgressBar(yt.setupProgress);
  //TODO(gavaletz) #39 Smarter video selection
  yt.frame = goog.dom.createDom('div', {'id': 'ytVideoDiv',
    'class': 'displayProgress'});
  page.progressSetDisplay(yt.frame);

  yt.xhr.send('/videoID/', 'GET');

  yt.setupProgress += 25;
  page.progressSetProgressBar(yt.setupProgress);

  yt.width = 640;
  yt.height = 360;
  yt.flashURL = 'http://www.youtube.com/apiplayer?enablejsapi=1&version=3';
  yt.requiredFlashVersion = '10';

  yt.flash = new goog.ui.media.FlashObject(yt.flashURL);

  yt.flash.setRequiredVersion(yt.requiredFlashVersion);

  if (yt.flash.hasRequiredVersion()) {
    yt.playerType = 'flash';
    yt.flash.setSize(yt.width, yt.height);
    yt.flash.setAllowScriptAccess(
        goog.ui.media.FlashObject.ScriptAccessLevel.ALWAYS);
    //Android hangs here.  Need more time to gather debuging info.
    yt.flash.render(yt.frame);

    yt.soundButton = new goog.ui.Button(nsi18n('ytSoundButton'),
        goog.ui.FlatButtonRenderer.getInstance());
    yt.soundButton.render(yt.frame);
    goog.events.listen(yt.soundButton, goog.ui.Component.EventType.ACTION,
        yt.soundToggle);
  }
  else {
    // http://code.google.com/apis/youtube/iframe_api_reference.html
    var iframeAPI = document.createElement('script');
    iframeAPI.type = 'text/javascript';
    iframeAPI.async = true;
    iframeAPI.src = 'http://www.youtube.com/player_api';
    var firstScript = document.getElementsByTagName('script')[0];
    firstScript.parentNode.insertBefore(iframeAPI, firstScript);
  }
};


/**
 * This is here because the last time we checked the YouTube player API still
 * had a bug in it.
 */
yt.soundToggle = function() {
  //TODO(gavaletz) waiting on YT bugfix as of 1/13/2011
  //if (yt.player.isMuted()) {
  if (yt.isMuted) {
    yt.player.unMute();
    yt.isMuted = false;
  }
  else {
    yt.player.mute();
    yt.isMuted = true;
  }
};


/**
 * Changes settings on the YouTube player once it is ready.
 *
 * This function seems to need to be in the global name space and that is why
 * it is not part of the yt namespace.
 */
onYouTubePlayerAPIReady = function() {
  yt.player = new YT.Player(yt.frame, {'height': yt.height, 'width': yt.width,
    'events': {'onReady': 'ytonIframePlayerReady',
      'onStateChange': 'ytonIframeStateChange', 'onError': 'ytonIframeError',
      'onPlaybackQualityChange': 'ytonIframeQualityChange'},
    'playerVars': { 'autoplay': 0, 'controls': 0 }
  });
  yt.setupProgress += 50;
  page.progressSetProgressBar(yt.setupProgress);
};
//used by the closure compiler to eport this function call
goog.exportProperty(window, 'onYouTubePlayerAPIReady', onYouTubePlayerAPIReady);


/**
 * Changes settings on the YouTube player once it is ready.
 */
yt.onIframePlayerReady = function() {
  clearTimeout(yt.timeoutID);
  yt.soundButton = new goog.ui.Button(nsi18n('ytSoundButton'),
      goog.ui.FlatButtonRenderer.getInstance());
  yt.soundButton.render(yt.frame);
  goog.events.listen(yt.soundButton, goog.ui.Component.EventType.ACTION,
      yt.soundToggle);
  if (yt.videoID && !yt.startTime) {
    yt.startExp();
  }
  yt.playerVersion = 'html5';
};
//used by the closure compiler to eport this function call
goog.exportProperty(window, 'ytonIframePlayerReady', yt.onIframePlayerReady);
